package com.kopipi.utils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import com.kopipi.annotation.Translate;
import com.kopipi.annotation.Translator;
import com.kopipi.cache.TranslatorCache;
import com.kopipi.cache.TranslatorKey;
import com.kopipi.cache.TranslatorValue;
import com.kopipi.exception.TranslationException;
import com.kopipi.translator.impl.CloneValue;
import com.kopipi.translator.impl.CopyReference;

public class ReflectUtils {
	
	/**
	 * Clones the value by invoking the clone() method.
	 * 
	 * @param value
	 * 			The value to clone.
	 * @return The clone.
	 * @throws TranslationException
	 * 			If clone() can't be invoked for any reason.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T clone(T value)
	throws TranslationException {
		Method method;
		try {
			method = value.getClass().getMethod("clone");
		} catch (Exception e) {
			throw new TranslationException("Can't get clone() method", e);
		}
		Object clone;
		try {
			clone = method.invoke(value);
		} catch (Exception e) {
			throw new TranslationException("Can't clone() value of class : "
					+ value.getClass().toString());
		}
		return (T) clone;
	}
	
	/**
	 * Returns the method that matches the 'value' for translation.
	 * This method must have the 'value' class as single parameter and return type.
	 * It can optionnaly be annotated @TranslationValue with the right 'from' and 'to' class.
	 * If not, the method will be found with its signature.
	 * 
	 * @param translatorClass
	 * 			The translator class containing the expected method.
	 * @param value
	 * 			The class of the value to translate.
	 * @param source
	 * 			The class of the bean containing the value to translate.
	 * @param target
	 * 			The class of the bean in which the the translated value will be injected.
	 * @return The method to invoke to translate the value.
	 * @throws TranslationException
	 * 			If no matching method is found.
	 */
	static Method getTranslator(Class<?> translatorClass, Class<?> value, Class<?> source, Class<?> target)
	throws TranslationException {
		Method[] methods = translatorClass.getDeclaredMethods();
		for (Method m : methods) {
			Translator annotation = m.getAnnotation(Translator.class);
			if (annotation == null) {
				continue;
			}
			if (annotation.from() == source && annotation.to() == target) {
				m = checkSignature(m, value);
				if (m == null) {
					continue;
				}
				return m;
			}
		}
		
		for (Method m : methods) {
			m = checkSignature(m, value);
			if (m == null) {
				continue;
			}
			return m;
		}
		throw new TranslationException("Can't find matching method translation for "
				+ value.toString() + " in " + translatorClass.toString());
	}
	
	/**
	 * Check the signature of the method so that it should match to the 'expected' class.
	 * 
	 * @param method
	 * 			The method to check.
	 * @param expected
	 * 			The class of the value to translate.
	 * @return The method if the method matches, or null if not.
	 */
	static Method checkSignature(Method method, Class<?> expected) {
		Class<?>[] params = method.getParameterTypes();
		if (params == null || params.length != 1) {
			return null;
		}
		if (!params[0].isAssignableFrom(expected)) {
			return null;
		}
		Class<?> type = method.getReturnType();
		if (type == void.class) {
			return null;
		}
		return method;
	}
	
	/**
	 * Translates the 'source' bean to the 'target' bean.
	 * All @Translate annotated attributes of the 'source' bean are translated
	 * and injected to the 'target' bean.
	 * 
	 * @param source
	 * 			The source bean to translate.
	 * @param target
	 * 			The target bean to translate into.
	 * @throws TranslationException
	 * 			If an error occurs.
	 */
	public static void translate(Object source, Object target)
	throws TranslationException {
		for (Field field : source.getClass().getDeclaredFields()) {
			Field f = getSymetricField(field, target.getClass());
			if (f == null) {
				throw new TranslationException("Source and target beans are not annotated symetricly : "
						+ source.toString() + " - " + target.toString());
			}
			Object value;
			try {
				field.setAccessible(true);
				value = field.get(source);
			} catch (Exception e) {
				throw new TranslationException("Can't get value : "
						+ source.getClass().toString() + "#" + field.getName(), e);
			}
			Object result = invokeTranslator(value, source.getClass(), target.getClass(), field);
			try {
				f.setAccessible(true);
				f.set(target, result);
			} catch (Exception e) {
				throw new TranslationException("Can't set value : "
						+ target.getClass().toString() + "#" + f.getName(), e);
			}
		}
	}
	
	static Object invokeTranslator(Object value, Class<?> source, Class<?> target, Field field) {
		Translate annotation = field.getAnnotation(Translate.class);
		if (annotation == null) {
			throw new TranslationException("Field is not annotated with @Translate : "
					+ target.toString() + "#" + field.getName());
		}
		TranslatorValue translator =
				TranslatorCache.getTranslator(source, field.getName(), annotation.id());
		if (translator == null) {
			Class<?> translatorClass = annotation.translator();
			Method translatorMethod = getTranslator(translatorClass, value.getClass(), source, target);
			
			Object translatorInstance;
			try {
				translatorInstance = translatorClass.newInstance();
			} catch (Exception e) {
				throw new TranslationException("Can't instanciate translator class : "
						+ translatorClass.toString(), e);
			}
			translator = new TranslatorValue(translatorInstance, translatorMethod);
			TranslatorKey key = TranslatorCache.key(source, field.getName(), annotation.id());
			
			TranslatorCache.setTranslator(key, translator);
		}
		try {
			return translator.getMethod().invoke(translator.getTranslator(), value);
		} catch (Exception e) {
			throw new TranslationException("Can't invoke translator :"
					+ translator.getTranslator().getClass() + "#" + translator.getMethod().getName(), e);
		}
	}
	
	public static void checkHalfSymetry(Class<?> source, Class<?> target) {
		for (Field field : source.getDeclaredFields()) {
			Field f = getSymetricField(field, target);
			if (f == null) {
				throw new TranslationException("Source and target beans are not annotated symetricly : "
						+ source.toString() + " - " + target.toString());
			}
		}
	}
	
	static Field getSymetricField(Field sourceField, Class<?> targetClass) {
		Translate annotation = sourceField.getAnnotation(Translate.class);
		boolean id = annotation.id() != null && annotation.id().length() > 0;
		Class<?> translator = annotation.translator();
		String attribute;
		if (id) {
			attribute = annotation.id();
		}
		else {
			attribute = sourceField.getName();
		}
		for (Field field : targetClass.getDeclaredFields()) {
			Translate a = field.getAnnotation(Translate.class);
			if (a == null) {
				continue;
			}
			if (a.translator() == CopyReference.class || a.translator() == CloneValue.class) {
				if (!field.getType().isAssignableFrom(sourceField.getType())) {
					continue;
				}
			}
			if (id) {
				if (a.id() != null && a.id().equals(attribute) && translator == a.translator()) {
					return field;
				}
			}
			else if (field.getName().equals(attribute) && translator == a.translator()) {
				return field;
			}
		}
		return null;
	}
	
}
